﻿#region Using directives

using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.IO;

#endregion

namespace BodyMapTypesGenerator {
  class Program {
    private const string R4Namespace = "Robotics4.NET";
    private const string RobletBaseClass = "RobletBase";
    private const string RobletMessageClass = "RobletMessage";
    private const string RobletInterfaceClass = "RobletInterface";

    /// <summary>
    /// Generate a class with the same members as the specified type.
    /// </summary>
    /// <param name="t">Type to clone</param>
    /// <param name="ns">CodeDOM namespace where to generate the type</param>
    static void GenerateClass(Type t, TextWriter output) {
        // Ensure types are generated only once.
        if (generatedTypes.Contains(t))
            return;

        // This type has been generated
        generatedTypes.Add(t);

        // Enum should be generated in a different way
        if (!t.IsEnum)
        {
            // Generate appropriate comment if it's a RobletMessage
            if (InputMessages.ContainsKey(t))
                output.WriteLine(string.Format("  // This is a message that can be sent to Roblet {0}",
                  InputMessages[t]));
            else if (OutputMessages.ContainsKey(t))
                output.WriteLine(string.Format("  // This is a message received from {0}",
                  OutputMessages[t]));

            // Prepares the declaration
            output.WriteLine("  public {0} {1} : {2} {{", t.IsValueType ? "struct" : "class", t.Name, (t.BaseType.Namespace == R4Namespace && t.BaseType.Name == RobletMessageClass) ? t.BaseType.Name : RobletMessageClass);

            // Only declared (not inherited) fields should be generated
            foreach (FieldInfo f in t.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly))
            {
                output.WriteLine("    public {0} {1};", f.FieldType.Name, f.Name);
                if (f.FieldType.Namespace != "System" &&
                  !generatedTypes.Contains(f.FieldType))
                {
                    if (f.FieldType.IsArray)
                        toGenerate.Enqueue(f.FieldType.GetElementType());
                    else
                        toGenerate.Enqueue(f.FieldType);
                }
            }
            output.WriteLine("  }");
        }
        else
        {
            Type bt = null;
            bool first = true;
            foreach (FieldInfo f in t.GetFields())
                if (f.FieldType != t)
                {
                    bt = f.FieldType;
                    break;
                }
            output.WriteLine("  public enum {0} {{", t.Name);

            foreach (FieldInfo f in t.GetFields())
            {
                if (f.FieldType == t)
                {
                    if (!first) output.WriteLine(",");
                    output.Write("    {0} = {1}", f.Name, string.Format("0x{0:x8}", (int)f.GetValue(null)));
                    first = false;
                }
            }
            output.WriteLine();
            output.WriteLine("  }");
        }
    }

    /// <summary>
    /// Input messages map (which message to which roblet)
    /// </summary>
    static Dictionary<Type, Type> InputMessages = new Dictionary<Type, Type>();
    /// <summary>
    /// Output messages map (which message to which roblet)
    /// </summary>
    static Dictionary<Type, Type> OutputMessages = new Dictionary<Type, Type>();
    /// <summary>
    /// Types already generated
    /// </summary>
    static List<Type> generatedTypes = new List<Type>();
    /// <summary>
    /// Types still to be generated
    /// </summary>
    static Queue<Type> toGenerate = new Queue<Type>();

    /// <summary>
    /// Process an assembly searching for Roblet messages to be generated.
    /// </summary>
    /// <param name="path">Path to the assembly to look for</param>
    static void ProcessAssembly(string path) {
        Assembly roblet = Assembly.LoadFile(new FileInfo(path).FullName);

      // Look for Roblet definitions and read interface
      foreach (Type t in roblet.GetTypes()) {
        if (t.BaseType.Namespace == R4Namespace && t.BaseType.Name == RobletBaseClass) {
          object[] attr = t.GetCustomAttributes(true);
          foreach (Attribute a in attr) {
            Type at = a.GetType();
            if (at.Namespace == R4Namespace && (at.Name == "InputMessageAttribute" || at.Name == "OutputMessageAttribute")) {
              FieldInfo f = at.GetField("Type");
              Type mt = f.GetValue(a) as Type;
              Dictionary<Type, Type> msgs = (at.Name == "InputMessageAttribute") ? InputMessages : OutputMessages;
              msgs[mt] = t;
            }
          }
        }
      }

      // Look for the messages
      foreach (Type t in roblet.GetTypes()) {
        Type bc = t.BaseType;
        if (bc.Namespace == R4Namespace && bc.Name == RobletMessageClass)
          if (InputMessages.ContainsKey(t) || OutputMessages.ContainsKey(t))
            toGenerate.Enqueue(t);
      }
    }

    /// <summary>
    /// Configuration of the program.
    /// </summary>
    struct Config {
      public List<string> assemblies;
      public TextWriter output;
      public string partial;
      public string ns;
    }

    /// <summary>
    /// Parse the command line. The function exits the program if no assembly
    /// is provided.
    /// </summary>
    /// <param name="args">Command line to process</param>
    /// <returns>The configuration options</returns>
    static Config ParseCommandLine(string[] args) {
      Config ret = new Config();
      // Default values
      ret.assemblies = new List<string>();
      ret.output = Console.Out;
      ret.partial = null;
      ret.ns = "RobletMessages";

      foreach (string param in args) {
        if (param.StartsWith("/")) {
          if (param.StartsWith("/out:")) {
            ret.output = File.CreateText(param.Substring(5));
          } else if (param.StartsWith("/partial:")) {
            ret.partial = param.Substring(9);
          } else if (param.StartsWith("/ns:")) {
            ret.ns = param.Substring(4);
          }
        } else
          ret.assemblies.Add(param);
      }
      return ret;
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="args"></param>
    static void Main(string[] args) {
      Config conf = ParseCommandLine(args);

      if (conf.assemblies.Count == 0) {
        Console.WriteLine("Usage: bmtg [/out:<file>] [/partial:<class name>] [/ns:<namespace>] assembly+");
        return;
      }

      conf.output.WriteLine("using System;");
      conf.output.WriteLine("using System.Collections;");
      conf.output.WriteLine("using System.Xml;");
      conf.output.WriteLine("using System.Xml.Serialization;");
      conf.output.WriteLine(string.Format("using {0};", R4Namespace));

      conf.output.WriteLine(string.Format("namespace {0} {{", conf.ns));

      conf.output.WriteLine(string.Format("public {0}class {1}{2} {{", conf.partial == null ? "" : "partial ", conf.partial, conf.partial == null ? "" : " : BodyMap"));

      foreach (string s in conf.assemblies)
        ProcessAssembly(s);

      if (conf.partial == null)
          conf.output.WriteLine("  protected static XMLSerializerPool SerializerPool = new XMLSerializerPool();");

      conf.output.WriteLine(@"
    private static Type[] ctxt;
    private static Hashtable ht;

    public static RobletMessage ReadMessage(XmlNode doc)
    {
      XmlSerializer s = SerializerPool[((Type)(ht[doc.Name])), """"];
      if (s == null)
        SerializerPool[((Type)(ht[doc.Name])), """"] = s = new XmlSerializer(((Type)(ht[doc.Name])), ctxt);

      return ((RobletMessage)(s.Deserialize(new XmlNodeReader(doc))));
    }

    public static RobletMessage ReadMessage(XmlDocument doc)
    {
      return ReadMessage(doc.ChildNodes[1]);
    }

    public RobletMessage GetInputMessage(string robletname, Type t)
    {
      XmlSerializer xmls = SerializerPool[typeof(RobletInterface), """"];
      if (xmls == null)
        SerializerPool[typeof(RobletInterface), """"] = xmls = new XmlSerializer(typeof(RobletInterface), ctxt);
      RobletInterface rif = ((RobletInterface)(xmls.Deserialize(new XmlNodeReader(RobletsW[robletname].ChildNodes[1]))));
      for (int i = 0; (i < rif.InputMessages.Length); i = (i + 1))
      {
        RobletMessage rmsg = rif.InputMessages[i].Message;
        if ((rmsg.GetType() == t))
          return rmsg;
      }
      return null;
    }

    public void Send(RobletMessage msg)
    {
      SendState(msg, ctxt);
    }
");

      MemoryStream m = new MemoryStream();
      TextWriter tw = new StreamWriter(m);
      // Clone types
      while (toGenerate.Count > 0)
        GenerateClass(toGenerate.Dequeue(), tw);
      tw.Flush();

      conf.output.WriteLine("    static {0}()", conf.partial == null ? "BodyMap" : conf.partial);
      conf.output.WriteLine("    {");
      conf.output.WriteLine("      ctxt = new Type[] {");
      for (int i = 0; i < generatedTypes.Count; i++)
          conf.output.WriteLine(string.Format("      typeof({0}),", generatedTypes[i].Name));
      conf.output.WriteLine("      typeof(HeartBeatMessage)");
      conf.output.WriteLine("      };");
      conf.output.WriteLine("      ht = new Hashtable();");
      conf.output.WriteLine("      for (int i = 0; (i < ctxt.Length); i = (i + 1))");
      conf.output.WriteLine("        ht[ctxt[i].Name] = ctxt[i];");
      conf.output.WriteLine("    }");

      conf.output.WriteLine("  }"); // Close class

      conf.output.WriteLine(System.Text.ASCIIEncoding.ASCII.GetString(m.GetBuffer(), 0, (int)m.Position));

      conf.output.WriteLine("}"); // Close namespace
      conf.output.Flush();
    }
  }
}
